﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.TeamFoundation;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.Framework.Server;
using Microsoft.TeamFoundation.WorkItemTracking.Client;
using System.ComponentModel;

namespace TfsRollUp.Core
{
    /// <summary>
    /// Manages the workitemstore and other objects that needs to be cached
    /// </summary>
    public class Service : ITeamFoundationService, IDisposable
    {
        private ThreadLocal<Dictionary<Uri, WorkItemStore>> _store = new ThreadLocal<Dictionary<Uri, WorkItemStore>>(() => new Dictionary<Uri, WorkItemStore>());
        private ThreadLocal<Dictionary<Uri, WorkItemFieldCollection>> _rollUpFields = new ThreadLocal<Dictionary<Uri, WorkItemFieldCollection>>(() => new Dictionary<Uri, WorkItemFieldCollection>());
        private TeamFoundationRequestContext _requestContext;
        private Receiver _receiver;
        private bool _disposed;

        /// <summary>
        /// Global settings to this service
        /// </summary>
        public Settings Settings { get; private set; }

        /// <summary>
        /// The tfs system account sid, used to guard against circular updates
        /// </summary>
        public string SystemAccountSid { get; private set; }

        /// <summary>
        /// Queue with workitem roots that is to be processed
        /// </summary>
        public BlockingCollection<WorkItem> WorkItemRootQueue { get; private set; }

        #region ITeamFoundationService
        /// <summary>
        /// Stop service
        /// </summary>
        /// <param name="systemRequestContext"></param>
        public void ServiceEnd(TeamFoundationRequestContext systemRequestContext)
        {
            TeamFoundationTrace.MethodEntry(systemRequestContext);
            _store = null;
            _rollUpFields = null;
            _receiver.ServiceOff();
            _receiver = null;
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Flush cached objects
        /// </summary>
        public void ServiceFlushCache()
        {
            TeamFoundationTrace.MethodEntry();
            _store = new ThreadLocal<Dictionary<Uri, WorkItemStore>>(() => new Dictionary<Uri, WorkItemStore>());
            var store = Store(_requestContext);
            _rollUpFields = new ThreadLocal<Dictionary<Uri, WorkItemFieldCollection>>(() => new Dictionary<Uri, WorkItemFieldCollection>());
            RollUpFields(_requestContext).AddStore(store);
            TeamFoundationTrace.MethodExit();
        }

        /// <summary>
        /// Initialize the workitemstore and analyze the workitemtypes
        /// </summary>
        /// <param name="systemRequestContext"></param>
        public void ServiceStart(TeamFoundationRequestContext systemRequestContext)
        {
            TeamFoundationTrace.MethodEntry(systemRequestContext);
            _requestContext = systemRequestContext;
            var store = Store(GetUriFromRequestContext(systemRequestContext));
            RollUpFields(_requestContext).AddStore(store);
            _receiver = new Receiver(this);
            if (_receiver.ServiceOn())
            {
                TeamFoundationTrace.Info("{0}.ServiceStart: The Receiver is turned on", Constants.TfsRollUpPlugin);
            }
            SystemAccountSid = _requestContext.UserContext.Identifier;
            WorkItemRootQueue = new BlockingCollection<WorkItem>();
            Task.Factory.StartNew(() => { PreProcessWorkItem(); });
            ReadSettings();
            TeamFoundationTrace.MethodExit();
        }

        private void ReadSettings()
        {
            var rs = _requestContext.GetService<TeamFoundationRegistryService>();
            Settings = Settings.FromString(rs.GetValue(_requestContext, "/" + Constants.TfsRollUpPlugin + "/Settings"));
            if (Settings == null)
            {
                Settings = new Settings();
                Settings.EnableRollUpProcesing = true;
                var locationService = _requestContext.GetService<TeamFoundationLocationService>();
                Settings.TswaLink = locationService.PublicAccessMapping.AccessPoint + "/web/UI/Pages/WorkItems/WorkItemEdit.aspx?id={0}";
            }
            Settings.PropertyChanged += new PropertyChangedEventHandler(SaveSettings);
        }

        private void SaveSettings(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            var rs = _requestContext.GetService<TeamFoundationRegistryService>();
            rs.SetValue(_requestContext, "/" + Constants.TfsRollUpPlugin + "/Settings", Settings.ToString());
        }

        #endregion ITeamFoundationService

        /// <summary>
        /// The WorkItemStore
        /// </summary>
        public WorkItemStore Store(Uri uri)
        {
            if (_store != null &&
                _store.IsValueCreated &&
                _store.Value != null &&
                _store.Value.ContainsKey(uri) &&
                _store.Value[uri] != null)
            {
                TeamFoundationTrace.Verbose("RollUp.Store: Store found for uri:{0} in cache", uri);
                return _store.Value[uri];
            }
            var store = InternalGetStore(uri);
            _store.Value.Add(uri, store);
            _rollUpFields.Value.Add(uri, new WorkItemFieldCollection(store));
            return store;
        }

        /// <summary>
        /// The WorkItemStore
        /// </summary>
        public WorkItemStore Store(TeamFoundationRequestContext requestContext)
        {
            return Store(GetUriFromRequestContext(requestContext));
        }

        /// <summary>
        ///  Alm RollUp fields
        /// </summary>
        public WorkItemFieldCollection RollUpFields(Uri uri)
        {
            if (_rollUpFields != null &&
                _rollUpFields.IsValueCreated &&
                _rollUpFields.Value != null &&
                _rollUpFields.Value.ContainsKey(uri))
            {
                return _rollUpFields.Value[uri];
            }
            else
            {
                var store = InternalGetStore(uri);
                _store.Value.Add(uri, store);
                _rollUpFields.Value.Add(uri, new WorkItemFieldCollection(store));
            }
            return _rollUpFields.Value[uri];
        }

        /// <summary>
        ///  Alm RollUp fields
        /// </summary>
        public WorkItemFieldCollection RollUpFields(TeamFoundationRequestContext requestContext)
        {
            return RollUpFields(GetUriFromRequestContext(requestContext));
        }


        /// <summary>
        /// Take all processing requests and have them convoyed into the WorkItemRootQueue
        /// Allow processing of the same root workitem within Constants.WorkItemTimeToLiveMilliSeconds
        /// </summary>
        private void PreProcessWorkItem()
        {
            TeamFoundationTrace.MethodEntry();
            var blockList = new Dictionary<string, DateTime>();
            foreach (var workItem in WorkItemRootQueue.GetConsumingEnumerable())
            {
                var workItemKey = workItem.CollectionTeamProjectIdKey();
                if (blockList.ContainsKey(workItemKey))
                {
                    var originalTime = blockList[workItemKey];
                    var delta = DateTime.UtcNow - originalTime;
                    if (delta.TotalMilliseconds > Constants.WorkItemTimeToLiveMilliSeconds)
                    {
                        // Reset timer
                        blockList[workItemKey] = DateTime.UtcNow;
                        Task.Factory.StartNew(() => ProcessWorkItem(workItem));
                    }
                    else // Block the workitem, i.e. throw it away
                    {
                        TeamFoundationTrace.Info("PreProcessWorkItem: WorkItem {0} showed up within {1} (ms) of the block-time and will be thrown out", workItem.Id, delta.TotalMilliseconds);
                    }
                }
                else
                {
                    blockList.Add(workItemKey, DateTime.UtcNow);
                    Task.Factory.StartNew(() => ProcessWorkItem(workItem));
                }
                BlockListHouseCleaning(blockList);
            }
            TeamFoundationTrace.MethodExit();
        }

        private void ProcessWorkItem(WorkItem workItem)
        {
            TeamFoundationTrace.MethodEntry();
            TeamFoundationTrace.Info("ProcessWorkItem: Finally, at last, something to do: processing WorkItem:{0}", workItem.Id);
            var fieldList = this.RollUpFields(workItem.Store.TeamProjectCollection.Uri).GetRollUpFieldList(workItem.Fields[CoreField.TeamProject].Value.ToString(), workItem.Type.Name);
            var tree = workItem.CalculateTree(fieldList, Settings);
            if (tree != null && tree.Count > 0)
            {
                var batchSaveErrors = workItem.SaveTree(tree);
                if (batchSaveErrors != null && batchSaveErrors.Length > 0)
                {
                    TeamFoundationTrace.Error("ProcessWorkItem: BatchSave failed. {0} errors occurred. Starting recalculation", batchSaveErrors.Length);
                    SendErrorsToTrace(batchSaveErrors);
                    tree = workItem.CalculateTree(fieldList, Settings);
                    batchSaveErrors = workItem.SaveTree(tree);
                    if (batchSaveErrors != null && batchSaveErrors.Length > 0)
                    {
                        TeamFoundationTrace.Error("ProcessWorkItem: Retry BatchSave failed. {0} errors occurred", batchSaveErrors.Length);
                        SendErrorsToTrace(batchSaveErrors);
                    }
                }
            }
            TeamFoundationTrace.MethodEntry();
        }

        private void SendErrorsToTrace(BatchSaveError[] batchSaveErrors)
        {
            foreach (var error in batchSaveErrors)
            {
                TeamFoundationTrace.Error("{0}.SendErrorsToTrace: WorkItem:{1} Exception:{2}", Constants.TfsRollUpPlugin, error.WorkItem.Id, error.Exception);
            }
        }

        private Uri GetUriFromRequestContext(TeamFoundationRequestContext requestContext)
        {
            TeamFoundationTrace.MethodEntry(requestContext);
            var locationService = requestContext.GetService<TeamFoundationLocationService>();
            var uri = new Uri(locationService.ServerAccessMapping.AccessPoint + "/" + requestContext.ServiceHost.Name);
            TeamFoundationTrace.MethodExit();
            return uri;
        }

        private WorkItemStore InternalGetStore(Uri uri)
        {
            TeamFoundationTrace.MethodEntry(uri);
            try
            {
                TeamFoundationTrace.Verbose("InternalGetStore: Creating workitemstore");
                var tfs = TfsTeamProjectCollectionFactory.GetTeamProjectCollection(uri);
                if (tfs != null)
                {
                    TeamFoundationTrace.Verbose("InternalGetStore: TfsCollection created");
                    var store = tfs.GetService<WorkItemStore>();
                    if (store == null)
                    {
                        var errorMessage = string.Format("InternalGetStore: Store not created for Uri:{0}", uri);
                        TeamFoundationApplication.Log(errorMessage, (int)Constants.EventIds.StoreNotCreated, EventLogEntryType.Warning);
                    }
                    else
                    {
                        TeamFoundationTrace.Verbose("InternalGetStore: Workitemstore created for Uri:{0}", uri);
                        TeamFoundationTrace.MethodExit();
                        return store;
                    }
                }
            }
            catch (Exception ex)
            {
                var error = string.Format("InternalGetStore:Unable to create workitemstore for Uri:'{0}'", uri);
                TeamFoundationApplication.LogException(error, ex);
            }
            TeamFoundationTrace.MethodExit();
            return null;
        }

        private static void BlockListHouseCleaning(Dictionary<string, DateTime> blockList)
        {
            if (blockList.Count > 512)
            {
                var timeNow = DateTime.UtcNow;
                foreach (var key in blockList.Keys)
                {
                    if (timeNow.Subtract(blockList[key]).TotalMilliseconds > Constants.WorkItemTimeToLiveMilliSeconds)
                    {
                        blockList.Remove(key);
                    }
                }
            }
        }
        #region IDisposable
        /// <summary>
        /// Kill
        /// </summary>
        /// <param name="disposing"></param>
        protected virtual void Dispose(bool disposing)
        {
            if (!_disposed)
            {
                ((IDisposable)_receiver).Dispose();
                _store.Dispose();
                _rollUpFields.Dispose();

                // Set the sentinel.
                _disposed = true;

                // Suppress finalization of this disposed instance.
                if (disposing)
                {
                    GC.SuppressFinalize(this);
                }
            }
        }

        /// <summary>
        /// Prepare kill
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
        }

        /// <summary>
        /// PrePreKill
        /// </summary>
        ~Service()
        {
            Dispose(false);
        }
        #endregion
    }
}
